package org.apache.ocean.main.search.tag;

import java.io.IOException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.ocean.main.search.tag.TagTransaction.Update;

public class TagTransactionsFile {
	//public static final String FILE_NAME = "tags.tlog";
	private String fileName;
	public static final byte ADD = 1;
	public static final byte DELETE = 2;
	public static final byte ADD_AND_DELETE = 3;
	public static final byte RECORD_OK = 0;
	public static final byte RECORD_DELETED = 100;
	private Directory directory;
	private IndexOutput output;
	private SortedMap<Long, TransactionLocation> locationMap = new TreeMap<Long, TransactionLocation>();
	private ReentrantLock writeLock = new ReentrantLock();

	// TODO: load existing transactions
	// TODO: handle if file does not exist yet
	public TagTransactionsFile(long indexVersion) throws IOException {
		fileName = "tags."+indexVersion+".tlog";
		output = directory.createOutput(fileName);
		load(null, true);
	}

	public static interface Callback {
		public void record(TagTransaction tagTransaction);
	}

	public void readAll(Callback callback) throws IOException {
		load(callback, false);
	}

	private void load(Callback callback, boolean putIntoMap) throws IOException {
		IndexInput indexInput = directory.openInput(fileName);
		try {
			int position = (int)indexInput.getFilePointer();
			byte status = indexInput.readByte();
			if (status == RECORD_OK) {
				long id = indexInput.readVLong();
				byte type = indexInput.readByte();
				Update addUpdate = null;
				Update deleteUpdate = null;
				if (type == ADD_AND_DELETE) {
					addUpdate = readUpdate(indexInput);
					deleteUpdate = readUpdate(indexInput);
				} else if (type == ADD) {
					addUpdate = readUpdate(indexInput);
				} else if (type == DELETE) {
					deleteUpdate = readUpdate(indexInput);
				}
				if (callback != null) {
					TagTransaction tagTransaction = new TagTransaction(id, addUpdate, deleteUpdate);
					callback.record(tagTransaction);
				}
				if (putIntoMap) {
					locationMap.put(id, new TransactionLocation(id, position));
				}
			}
		} finally {
			indexInput.close();
		}
	}

	public int size() throws IOException {
		return (int) directory.fileLength(fileName);
	}

	public static class TransactionLocation {
		public long id;
		public int position;

		public TransactionLocation(long id, int position) {
			this.id = id;
			this.position = position;
		}
	}

	public TransactionLocation write(TagTransaction tagTransaction) throws IOException {
		writeLock.lock();
		try {
			int position = (int) output.getFilePointer();
			output.writeByte(RECORD_OK);
			output.writeVLong(tagTransaction.id);
			if (tagTransaction.add != null && tagTransaction.delete != null) {
				output.writeByte(ADD_AND_DELETE);
				writeUpdate(tagTransaction.add);
				writeUpdate(tagTransaction.delete);
			}
			if (tagTransaction.add != null) {
				output.writeByte(ADD);
				writeUpdate(tagTransaction.add);
			}
			if (tagTransaction.delete != null) {
				output.writeByte(DELETE);
				writeUpdate(tagTransaction.delete);
			}
			TransactionLocation transactionLocation = new TransactionLocation(tagTransaction.id, position);
			locationMap.put(tagTransaction.id, transactionLocation);
			return transactionLocation;
		} finally {
			writeLock.unlock();
		}
	}

	private static Update readUpdate(IndexInput indexInput) throws IOException {
		String field = indexInput.readString();
		String text = indexInput.readString();
		int num = indexInput.readVInt();
		int[] array = new int[num];
		for (int x = 0; x < num; x++) {
			array[x] = indexInput.readVInt();
		}
		return new Update(new Term(field, text), array);
	}

	private void writeUpdate(Update update) throws IOException {
		output.writeString(update.term.field());
		output.writeString(update.term.text());
		output.writeVInt(update.docs.length);
		for (int doc : update.docs) {
			output.writeVInt(doc);
		}
	}

	public void delete(long id) throws IOException {
		writeLock.lock();
		try {
			TransactionLocation transactionLocation = locationMap.get(id);
			output.seek(transactionLocation.position);
			output.writeByte(RECORD_DELETED);
			locationMap.remove(id);
		} finally {
			writeLock.unlock();
		}
	}

	public void close() throws IOException {
		writeLock.lock();
		try {
			if (output != null)
				output.close();
			directory.close();
		} finally {
			writeLock.unlock();
		}
	}
}
